package cn.shadow.mines;


import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 扫雷
 */
@Slf4j
public class Mines {

    private int row; //行
    private int col; //列
    private Map<Integer, MinesPoint> table;  //保存了所有单元格
    private int minus;                //雷数量
    private boolean isOver = false;   //是否结束
    private boolean firstClick;       //是否是第一次点击
    private List<MinesPoint> changePoints = new ArrayList<>(); //每次点击时变化的单元格的集合
    private List<MinesPoint> currentMinus = new ArrayList<>(); //当前局的雷的坐标集合
    private List<MinesPoint> shine = new ArrayList<>(); //闪烁的单元格集合
    private ObjectMapper mapper = new ObjectMapper();

    public Mines(int row, int col, int minus) {
        this.row = row;
        this.col = col;
        this.minus = minus;
        this.table = new HashMap<>(row * col);
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                table.put(1000 * i + j, new MinesPoint(i, j, 0));
            }
        }
        firstClick = true;
    }

    public List<MinesPoint> getChangePoints() {
        return changePoints;
    }

    private boolean isFirstClick() {
        return firstClick;
    }

    public int getRow() {
        return row;
    }

    public int getCol() {
        return col;
    }

    public int getMinus() {
        return minus;
    }

    public Map<Integer, MinesPoint> getTable() {
        return table;
    }

    public List<MinesPoint> getShine() {
        return shine;
    }


    public List<MinesPoint> getCurrentMinus() {
        return currentMinus;
    }


    /**
     * 初始化
     *
     * @param firstRow 第一次点击的位置 行
     * @param firstCol 列
     */
    public void init(int firstRow, int firstCol) {
        putMines(firstRow, firstCol); //放置雷
        countEachPoint(); //计算每个单元格的值
        log.info("初始化扫雷格子{}", currentMinus);
    }

    /**
     * 根据第一次点击初始化雷的位置
     *
     * @param firstRow 第一次点击的位置 行
     * @param firstCol 列
     */
    private void putMines(int firstRow, int firstCol) {
        for (int count = 0; count < minus; ) {

            int row = (int) (Math.random() * this.row);
            int col = (int) (Math.random() * this.col);

            if (canSetMinus(row, col, firstRow, firstCol)) { //不能将雷放入初始化点的四周,因为这个点一定为空.
                continue;
            }
            MinesPoint point = getPoint(row, col);
            if (point.getValue() == 0) {
                point.setValue(9);
                count++;
                currentMinus.add(point);//将雷的坐标点放入当前的雷的集合中
            }
        }
    }


    /**
     * @param row
     * @param col
     * @param firstRow
     * @param firstCol
     * @return 判断一点是否在另一点的九宫格内
     */
    private static boolean canSetMinus(int row, int col, int firstRow, int firstCol) {
        for (int k = firstRow - 1; k <= firstRow + 1; k++) {
            for (int l = firstCol - 1; l <= firstCol + 1; l++) {
                if (k == row && l == col) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 全局数据的重置
     */
    public void reset() {
        for (MinesPoint point : table.values()) {
            point.setValue(0);
            point.setVisible(false);
            point.setFlagged(false);
        }
        currentMinus.clear();
        changePoints.clear();
        isOver = false;
        firstClick = true;
    }

    /**
     * @return 是否结束
     * 结束条件:
     * 1.踩雷了
     * 2.胜利了
     */
    public boolean isOver() {
        return isOver || win();
    }

    /**
     * 赢的条件:所有非雷点都被打开了
     *
     * @return 是否胜利
     */
    public boolean win() {
        int count = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                MinesPoint point = getPoint(i, j);
                if (point.isVisible()) {
                    count++;
                }
            }
        }
        return row * col - count == minus;
    }


    /**
     * 计算每一个单元的周围的雷的数量,得到这个数字
     */
    private void countEachPoint() {
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                MinesPoint point = getPoint(i, j);
                if (point.getValue() == 9) { //如果是雷,跳过
                    continue;
                }
                int count = 0; //计算周围雷的个数
                for (int k = i - 1; k <= i + 1; k++) {
                    for (int l = j - 1; l <= j + 1; l++) {
                        if (isOverRanged(k, l)) {
                            continue;
                        }
                        MinesPoint temp = getPoint(k, l);
                        if (temp.getValue() == 9) {
                            count++;
                        }
                    }
                }
                point.setValue(count); //根据周围雷的个数设置这个单元格的数值
            }
        }
    }

    /**
     * @param i 行
     * @param j 列
     * @return 根据行列坐标获得指定的单元格对象
     */
    public MinesPoint getPoint(int i, int j) {
        if (isOverRanged(i, j)) { //越界
            return null;
        }
        return table.get(1000 * i + j);
    }

    /**
     * 点击某点坐标,如果为数字,就显示该数字,如果是雷,就结束游戏
     * 如果是空的,就点开周围所有的点
     *
     * @param row 行
     * @param col 列
     */
    public MinesData click(int row, int col) {
        if (isOver()) {
            return null;
        }
        MinesPoint point = getPoint(row, col); //得到点击的点坐标
        if (point == null) { //先判断该点是不是存在
            return null;
        }

        beforeClick(row, col); //点击之前

        if (point.isVisible()) {     //点击可见的单元格
            clickVisiblePoint(point);
        } else {
            clickHiddenPoint(point); //点击隐藏的单元格
        }
        return afterClick();  //点击后
    }


    /**
     * 点击隐藏的点
     *
     * @param point 点
     */
    private void clickHiddenPoint(MinesPoint point) {

        if (point.isFlagged()) { //被旗子标记了
            return;
        }

        if (point.getValue() == 9) { //点到雷了
            isOver = true;
            return;
        }
        clickPoint(point);
    }

    /**
     * 点击之前
     * 1.判断是不是第一次点击,如果是,就初始化
     * 2.将上一次点击的产生的数据清除掉
     *
     * @param row
     * @param col
     */
    private void beforeClick(int row, int col) {

        if (isFirstClick()) {
            init(row, col);
            firstClick = false;
        }
    }

    /**
     * 在点击后,将那些为-1的点都设置为 0
     */
    private MinesData afterClick() {
        //将数值为-1的都设置为0
        for (int i = 0; i < this.row; i++) {
            for (int j = 0; j < this.col; j++) {
                MinesPoint temp = getPoint(i, j);
                if (temp.getValue() == -1) {
                    temp.setValue(0);
                }
            }
        }
        List<MinesPoint> current = new ArrayList<>(getChangePoints()); //当前要发送的所有的坐标对象的集合
        MinesData data = new MinesData();
        if (isOver()) { //失败结束了

            if (win()) {
                data.setState(1);
            } else {
                data.setState(2);
            }
            List<MinesPoint> rest = new ArrayList<>(); //剩余的所有点
            for (MinesPoint restPoint : getTable().values()) {
                if (!restPoint.isVisible() && !(restPoint.getValue() == 9)) { //没有打开且不为雷的点都设置为打开
                    restPoint.setVisible(true);
                    rest.add(restPoint);
                }
            }
            //获得所有雷的数据,发送过去
            List<MinesPoint> currentMinus = getCurrentMinus();
            current.addAll(currentMinus);
            current.addAll(rest);
        }
        if (current.size() == 0 && getShine().size() > 0) {
            data.setState(3);
            current.addAll(new ArrayList<>(getShine()));
        }
        String list = null;
        try {
            list = mapper.writeValueAsString(current);
        } catch (IOException e) {
            e.printStackTrace();
        }
        data.setList(list);
        changePoints.clear(); //清除掉改变的单元格的集合
        shine.clear();        //清掉要闪烁的单元格
        return data;
    }


    /**
     * 重点代码
     * 点击非雷的单元格
     * 点击一个点时,如果这个点是空白(值为0)的,将这个点的值设置为-1,遍历周围所有的点,重复这个操作
     * 如果不是空白的,判断是否被点开过,如果没有被点开过,就点开.
     */
    private void clickPoint(MinesPoint point) {

        int pi = point.getRow();
        int pj = point.getCol();

        if (point.getValue() == 0) {
            point.setValue(-1); //将这个点设置为-1
            for (int i = pi - 1; i <= pi + 1; i++) {
                for (int j = pj - 1; j <= pj + 1; j++) {
                    if (isOverRanged(i, j)) { //越界了
                        continue;
                    }
                    MinesPoint temp = getPoint(i, j);
                    if (temp.isFlagged()) { //如果被插旗了，跳过
                        continue;
                    }
                    clickPoint(temp);
                }
            }
        }

        //如果没有被标记且没有被点开
        if (!point.isVisible() && !point.isFlagged()) {
            point.setVisible(true);  //点开这个点
            changePoints.add(point); //添加到点开的集合中
        }
    }

    /**
     * @param row 行
     * @param col 列
     * @return 是否越界
     */
    private boolean isOverRanged(int row, int col) {
        return row < 0 || row > this.row - 1 || col < 0 || col > this.col - 1;
    }


    /**
     * 点击可见的单元格
     */
    private void clickVisiblePoint(MinesPoint point) {

        int r = point.getRow();
        int c = point.getCol();
        int value = point.getValue();
        int count = 0;//旗子的数量

        //计算该点9宫格内旗子的数量
        for (int i = r - 1; i <= r + 1; i++) {
            for (int j = c - 1; j <= c + 1; j++) {
                if (isOverRanged(i, j)) {
                    continue;
                }
                MinesPoint temp = getPoint(i, j);
                if (temp.isFlagged()) { //发现旗子
                    count++;  //数量增加
                }
                if (!temp.isVisible() && !temp.isFlagged()) { //未被点开且未被标记,添加到闪烁集合中
                    shine.add(temp);
                }
            }
        }

        if (value == count) {    //旗子个数等于于雷的个数,点开周围所有的雷
            for (int i = r - 1; i <= r + 1; i++) {
                for (int j = c - 1; j <= c + 1; j++) {
                    if (isOverRanged(i, j)) {
                        continue;
                    }

                    MinesPoint temp = getPoint(i, j);

                    if (temp.isFlagged()) { //该点被标记了,绕过
                        continue;
                    }

                    if (!temp.isFlagged() && temp.getValue() == 9) {  //没有被标记的雷
                        isOver = true; //炸了,结束了
                        return;
                    }

                    clickPoint(temp); //调用点击的方法
                }
            }
        } else if (value < count) { //标记的个数小于雷的个数
            shine.clear(); //清掉
        }
    }

    /**
     * 返回被标记的单元格
     *
     * @param row 行
     * @param col 列
     * @return 被标记的单元格对象
     */
    public MinesPoint flagged(int row, int col) {
        MinesPoint temp = getPoint(row, col);
        if (temp == null || temp.isVisible()) {
            return null;
        }
        if (temp.isFlagged()) {
            temp.setFlagged(false);
        } else {
            temp.setFlagged(true);
        }
        return temp;
    }
}
